package com.bungarus.sdk;


import com.bungarus.sdk.exception.ConnectionFailedException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

/**
 * Created by tang on 2019/1/30.
 */
abstract public class SmarterClient extends Client {
    private final static String DEFAULT_SMART_URL = "http://smart.bungarus.com:9090/routes";
    private final static String DEFAULT_ACCESS_PORT_VALUE = "9080";

    private final static String DEFALUT_HOST_STR = "access.host";
    private final static String ACCESS_PORT_STR = "access.port";
    private final static String SMART_URL_STR = "smart.url";

    private static final long TIMEOUT = 3;
    private ExecutorService threadPool = Executors.newFixedThreadPool(1);

    private CloseableHttpClient client;

    private int accessPort = -1;
    private String accessHost;
    private String smartUrl = null;

    public static final Callback<List<InetSocketAddress>, Boolean> DEFAULT_CALLBACK = null;

    public SmarterClient() {
        initEnv();
        init();
    }

    private void initEnv() {
        smartUrl = System.getProperty(SMART_URL_STR, DEFAULT_SMART_URL);
        accessPort = Integer.parseInt(System.getProperty(ACCESS_PORT_STR, DEFAULT_ACCESS_PORT_VALUE));
        accessHost = System.getProperty(DEFALUT_HOST_STR,"");
    }

    private void init() {
        PoolingHttpClientConnectionManager clientConnectionManager = new PoolingHttpClientConnectionManager();
        clientConnectionManager.setMaxTotal(Integer.MAX_VALUE);
        clientConnectionManager.setDefaultMaxPerRoute(Integer.MAX_VALUE);

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout((int) TIMEOUT * 2)
                .setConnectTimeout((int) TIMEOUT * 3)
                .setSocketTimeout((int) TIMEOUT * 3)
                .build();

        client = HttpClients.custom()
                .setKeepAliveStrategy((HttpResponse response, HttpContext context) -> {
                    HeaderElementIterator it = new BasicHeaderElementIterator
                            (response.headerIterator(HTTP.CONN_KEEP_ALIVE));
                    while (it.hasNext()) {
                        HeaderElement he = it.nextElement();
                        String param = he.getName();
                        String value = he.getValue();
                        if (value != null && param.equalsIgnoreCase
                                ("timeout")) {
                            return Long.parseLong(value) * 1000;
                        }
                    }
                    return TIMEOUT * 2 * 1000;
                })
                .setConnectionManager(clientConnectionManager)
                .setDefaultRequestConfig(requestConfig)
                .build();
    }

    public void smartConnect(Callback<List<InetSocketAddress>, Boolean> callback) {
        if(null != callback) {
            threadPool.submit(
                    () -> {
                        //get server list from remote service
                        List<InetSocketAddress> servers = getBungarusServerList();
                        //call callback;
                        callback.call(servers);
                    });
        }
        else {
            innerConnect();
        }
    }

    private void innerConnect() {
        Future<List<InetSocketAddress>> future = threadPool.submit(
                () -> {
                    //get server list from remote service
                    List<InetSocketAddress> servers = getBungarusServerList();
                    return servers;
                });
        //connect one of servers directly;
        try {
            List<InetSocketAddress> servers = future.get(TIMEOUT, TimeUnit.SECONDS);
            if(null != servers && !servers.isEmpty()) {
                super.connect(servers);
            }
        }
        catch (InterruptedException | ExecutionException | TimeoutException e ) {
            e.printStackTrace();
            if(!future.isCancelled()) {
                future.cancel(true);
            }
        }
        catch (ConnectionFailedException e) {
            //Retrieve the server list from smart route when all servers are not available, until
            //a server is connected to.
            innerConnect();
        }
    }

    public EndUser getLoggedIn() {
        return super.loggedInUser;
    }

    @Override
    public void close() {
        threadPool.shutdown();
        super.close();
    }

    private List<InetSocketAddress> getBungarusServerList() {
        if(accessHost.length() > 0) {
            //To be given the specific access host/ip, we try to connect to it.
            List<InetSocketAddress> servers = new ArrayList<>();
            servers.add(new InetSocketAddress(accessHost, accessPort));
            return servers;
        }
        else {
            HttpGet getMethod = new HttpGet(smartUrl);
            try {
                CloseableHttpResponse response = client.execute(getMethod);
                HttpEntity entity = response.getEntity();
                if (entity.getContentType().getValue().contains("application/json")) {
                    String strEntity = EntityUtils.toString(entity, "utf-8");

                    ObjectMapper objectMapper = new ObjectMapper();
                    JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, String.class);
                    List<String> ips = objectMapper.readValue(strEntity, javaType);

                    if (null != ips && !ips.isEmpty()) {
                        List<InetSocketAddress> servers = new ArrayList<>(ips.size());
                        ips.stream().forEach(server -> servers.add(new InetSocketAddress(server, accessPort)));
                        return servers;
                    }
                }
            }
            catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}
